/*******************************************************************************
 * Copyright (c) 2011 Irineu Martins de Lima Moura (imlm2).
 * This file is part of JBeat.
 * 
 *     JBeat is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     JBeat is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU Lesser General Public License for more details.
 * 
 *     You should have received a copy of the GNU Lesser General Public License
 *     along with JBeat.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Irineu Martins de Lima Moura (imlm2) - initial API and implementation
 ******************************************************************************/
package br.ufpe.cin.imlm2.jbeat.soot;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

import soot.SootClass;
import soot.SootMethod;
import soot.options.Options;

import br.ufpe.cin.imlm2.jbeat.IExceptionFlowAnalysis;
import br.ufpe.cin.imlm2.jbeat.soot.common.SootClassResolver;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.tags.ThrowableSetTag;
import br.ufpe.cin.imlm2.util.Naming;

public class ExceptionAnalysisImpl implements IExceptionFlowAnalysis {

	private static final ExceptionAnalysisRunner RUNNER = new ExceptionAnalysisRunner();

	private List<SootClass> targetSet;

	private ExceptionAnalysisImpl(List<SootClass> targetSet) {
		super();
		this.targetSet = targetSet;
	}

	/**
	 * This class encapsulates all the details necessary to setup the <br>
	 * arguments for the exception flow analysis. <br>
	 * It uses the builder pattern so that you may chain calls to setup the
	 * wanted arguments. <br>
	 * A typical usage example would be: <br>
	 * <br>
	 * <code>
	 * String mainClass = ...; <br>
	 * String classpath = ...; <br>
	 * ExceptionAnalysisRunner ear = ExceptionAnalysisImpl.getRunner(); <br>
	 * ear.preciseCallGraph(true).excludeClass("java.lang.String");  <br>
	 * ExceptionAnalysisImpl analysis = ear.runAnalysis(mainClass,classpath);
	 * </code>
	 * 
	 * @author irineu
	 * 
	 */
	public static class ExceptionAnalysisRunner {
		private boolean spark = false;
		private boolean trapInspection = true;
		private boolean allReachable = true;
		private boolean clinits = false;
		private boolean autoGenerated = false;
		private List<String> classesExcludeList = new ArrayList<String>();
		private List<String> packagesExcludeList = new ArrayList<String>();
		private boolean analyzeRT = true;
		private boolean withReflection = false;
		private boolean rtLibs = true;

		/**
		 * Runs the analysis over the given main class and all reachables
		 * classes in the classpath. <br>
		 * Each entry in the classpath must be separated according to the
		 * client's OS specific file separator. <br>
		 * The objects returned by this method may safely be cast to
		 * <code>ExceptionAnalysisImpl</code> <br>
		 * If a class needed for the analysis is not found then a
		 * RuntimeException will be thrown. <br>
		 * 
		 * @param mainClass
		 * @param classpath
		 * @return
		 */
		public IExceptionFlowAnalysis runAnalysis(String mainClass,
				String classpath) {
			Options ops = Options.v();
			ops.set_whole_program(true);
			ops.set_app(true);
			if (spark)
				ops.setPhaseOption("cg.spark", "enabled:true");
			else
				ops.setPhaseOption("cg.cha", "enabled:true");
			ops.setPhaseOption("cg", "implicit-entry:false");
			ops.set_allow_phantom_refs(true);

			if (allReachable)
				ops.setPhaseOption("cg", "all-reachable:true");
			else
				ops.setPhaseOption("cg", "all-reachable:false");

			if (!this.withReflection)
				this.packagesExcludeList.add("java.lang.reflect.*");

			/*
			 * EmptyTrapRemover etr = new EmptyTrapRemover();
			 * PackManager.v().getPack("wjtp").add(new Transform("wjtp.etr",
			 * etr));
			 * 
			 * if (trapInspection) { TrapInspector ti = new
			 * TrapInspector(this.classesExcludeList, this.packagesExcludeList,
			 * this.rtLibs); PackManager.v().getPack("wjtp") .add(new
			 * Transform("wjtp.ti", ti)); }
			 * 
			 * if (this.analyzeRT) { GeneratesCalculator gc = new
			 * GeneratesCalculator( this.classesExcludeList,
			 * this.packagesExcludeList, this.rtLibs);
			 * PackManager.v().getPack("wjtp") .add(new Transform("wjtp.gc",
			 * gc)); }
			 * 
			 * RaisesCalculator rc = new
			 * RaisesCalculator(this.classesExcludeList,
			 * this.packagesExcludeList, this.rtLibs);
			 * PackManager.v().getPack("wjtp").add(new Transform("wjtp.rc",
			 * rc)); PropagatesCalculator pc = new PropagatesCalculator(
			 * this.classesExcludeList, this.packagesExcludeList, this.rtLibs,
			 * clinits, autoGenerated); PackManager.v().getPack("wjtp").add(new
			 * Transform("wjtp.pc", pc));
			 * 
			 * String[] args = { mainClass, "-f", "J", "-cp", classpath };
			 * soot.Main.main(args);
			 */
			Collection<SootClass> classes = SootClassResolver.resolveClasses(
					classesExcludeList, packagesExcludeList, rtLibs);
			return new ExceptionAnalysisImpl(new ArrayList<SootClass>(classes));
		}

		/**
		 * Use the more precise but slower CallGraph construction algorithm or
		 * not. <br>
		 * Setting this to true will likely decrease the number of possible
		 * exceptions <br>
		 * generated by method found by the analysis <br>
		 * Default value is false
		 * 
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner preciseCallGraph(boolean b) {
			this.spark = b;
			return this;
		}

		/**
		 * Use the handler inspection algorithm as described by Fu & Ryder or
		 * not Defaults to true
		 * 
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner trapInspection(boolean b) {
			this.trapInspection = b;
			return this;
		}

		/**
		 * Consider unreachable methods in the classpath when constructing the
		 * callgraph or not Defaults to true
		 * 
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner allReachableEntryPoints(boolean b) {
			this.allReachable = b;
			return this;
		}

		/**
		 * Consider static initializers in the callgraph or not. Setting this to
		 * true most likely will drastically increase the number of possible
		 * exceptions generated by a method found by the analysis, thus it is
		 * recommended to use the default values. Defaults to false
		 * 
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner staticInitializars(boolean b) {
			this.clinits = b;
			return this;
		}

		/**
		 * Consider additional methods generated by the compiler or not Defaults
		 * to false Most likely this WILL NOT exclude all possible
		 * auto-generated methods.
		 * 
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner autoGeneratedMethods(boolean b) {
			this.autoGenerated = b;
			return this;
		}

		/**
		 * Don't consider classes with names passed as parameters in the
		 * analysis. Null,empty and repeated Strings are ignored
		 * 
		 * @param fullyQualifiedClassesNames
		 * @return
		 */
		public ExceptionAnalysisRunner excludeClasses(
				List<String> fullyQualifiedClassesNames) {
			for (String string : fullyQualifiedClassesNames) {
				this.excludeType(string);
			}
			return this;
		}

		/**
		 * Don't consider the class with name passed as parameter in the
		 * analysis. If the name passed as parameter is not a valid JVM fully
		 * classified type name then it will be discarded. Null,empty and
		 * repeated Strings are ignored
		 * 
		 * @param fullyQualifiedTypeName
		 * @return
		 */
		public ExceptionAnalysisRunner excludeType(String fullyQualifiedTypeName) {
			if (fullyQualifiedTypeName != null
					&& !fullyQualifiedTypeName.isEmpty()
					&& !this.classesExcludeList
							.contains(fullyQualifiedTypeName)
					&& Naming.isValidJVMName(fullyQualifiedTypeName))
				this.classesExcludeList.add(fullyQualifiedTypeName);
			return this;
		}

		/**
		 * Don't consider the classes from the given packaged in the analysis
		 * Null,empty and repeated Strings are ignored
		 * 
		 * @param fullyQualifiedPackageName
		 * @return
		 */
		public ExceptionAnalysisRunner excludePackage(
				String fullyQualifiedPackageName) {
			if (fullyQualifiedPackageName != null
					&& !fullyQualifiedPackageName.isEmpty()
					&& !this.packagesExcludeList
							.contains(fullyQualifiedPackageName)
					&& Naming.isValidJVMName(fullyQualifiedPackageName))
				this.packagesExcludeList.add(fullyQualifiedPackageName);
			return this;
		}

		/**
		 * Consider exceptions thrown by the runtime? (i.e.
		 * ArrayIndexOutOfBounds,NullPointer, etc) Defaults to <code>true</code>
		 * 
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner considerThrownByRuntime(boolean b) {
			this.analyzeRT = b;
			return this;
		}

		/**
		 * Consider exceptions thrown by methods in classes belonging to the
		 * java.lang.reflect? Defaults to <code>false</code>
		 * 
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner considerReflectionPackages(boolean b) {
			this.withReflection = b;
			return this;
		}

		/**
		 * Consider exceptions thrown by methods in the Java Runtime Libraries?
		 * Defaults to <code>false</code>
		 * 
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner considerRuntimeLibraries(boolean b) {
			this.rtLibs = b;
			return this;
		}
	}

	/**
	 * Returns the ExceptionAnalysisRunner object necessary to setup and run
	 * this analysis.
	 * 
	 * @return
	 */
	public static ExceptionAnalysisRunner getRunner() {
		return RUNNER;
	}

	public Collection<SootClass> getSootClassExceptionsThrownBy(
			String fullyQualifiedClassName, String fullyQualifiedMethName)
			throws Exception {
		for (SootClass c : this.targetSet) {
			if (c.equals(fullyQualifiedClassName)) {
				try {
					SootMethod m = c.getMethod(fullyQualifiedMethName);
					return this.getExceptionsThrownBy(m);
				} catch (RuntimeException e) {
					throw new MethodNotFoundException("Method with name "
							+ fullyQualifiedMethName + " wasn't found in type "
							+ fullyQualifiedClassName);
				}
			}
		}
		throw new TypeNotFoundException("Type with name + "
				+ fullyQualifiedClassName + " wasn't found");
	}

	public Collection<SootClass> getExceptionsThrownBy(SootMethod m) {
		ThrowableSetTag tag = (ThrowableSetTag) m
				.getTag(ThrowableSetTag.TAG_NAME);
		if (tag == null) {
			throw new RuntimeException(
					"ThrowableSetTag wasn't found for method "
							+ m.getSignature()
							+ ". This is probably a bug or you are trying to retrieve the exceptions from a method "
							+ "not considered in the analysis.");
		}
		HashSet<SootClass> classes = new HashSet<SootClass>(
				tag.getThrowableSet());
		return classes;
	}

	public Collection<String> getExceptionsThrownBy(
			String fullyQualifiedClassName, String fullyQualifiedMethName)
			throws Exception {
		Collection<SootClass> classes = this.getSootClassExceptionsThrownBy(
				fullyQualifiedClassName, fullyQualifiedMethName);
		Collection<String> classesNames = new ArrayList<String>();
		for (SootClass s : classes) {
			classesNames.add(s.getName());
		}
		return classesNames;
	}

}
